﻿using System;
using System.IO.Packaging;

namespace OPCDiffCore
{
    class LogicalAnalyzeModule : IAnalyzeModule
    {
        #region Event

        /// <summary>
        /// Raise when a part has been analyzed.
        /// </summary>
        public event EventHandler<PartEventArgs> PartAnalyzed;

        /// <summary>
        /// Raise just before a part is analyzed.
        /// </summary>
        public event EventHandler<PartEventArgs> PartAnalyzing;

        #endregion

        public AnalyzeLevel GetCheckType()
        {
            return AnalyzeLevel.Logical;
        }

        public PackageDiff RunAnalyze(AnalyzeConfiguration configuration)
        {
            return new PackageDiff(CheckDifference(configuration.OriginalFile,
                configuration.CompareFile, configuration.AnalyzeLevel));
        }

        /// <summary>
        /// Returns the differences between the two specified packages.
        /// </summary>
        /// <param name="pkgOriginal">The package considered as the original.</param>
        /// <param name="pkgDiff">The package considered as the modified one to be tested 
        /// against the original one.</param>
        /// <returns>All the part differences.</returns>
        private PartDiffCollection CheckDifference(string pathOriginal, string pathDiff, AnalyzeLevel level)
        {
            PartDiffCollection retDiffElements = new PartDiffCollection();

            using (Package pkgOriginal = Package.Open(pathOriginal))
            {
                using (Package pkgDiff = Package.Open(pathDiff))
                {
                    // Check properties
                    PartDiff corePropDiff = CheckPropertiesDiff(pkgOriginal, pkgDiff);
                    if (corePropDiff != null && corePropDiff.Count > 0)
                        retDiffElements.Add(corePropDiff);

                    // Check each Part diff
                    PartDiffCollection partsDiff = CheckPartsDiff(pkgOriginal, pkgDiff, level);
                    foreach (PartDiff diff in partsDiff)
                        retDiffElements.Add(diff);
                }
            }

            return retDiffElements;
        }

        #region Check Properties

        private PartDiff CheckPropertiesDiff(Package pkgOriginal, Package pkgDiff)
        {
            if (pkgOriginal.PackageProperties.Equals(pkgDiff))
                return null;

            PartDiff retPartDiff = new PartDiff(GetCorePropertiesPart(pkgOriginal),
                "application/vnd.openxmlformats-package.core-properties+xml", DiffType.Modified);

            PackageProperties propsOriginal = pkgOriginal.PackageProperties;
            PackageProperties propsDiff = pkgDiff.PackageProperties;

            DiffElement diff;

            if ((diff = CompareProperty(propsOriginal.Category, propsDiff.Category, "Category")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.ContentStatus, propsDiff.ContentStatus, "ContentStatus")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.ContentType, propsDiff.ContentType, "ContentType")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Created, propsDiff.Created, "Created")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Creator, propsDiff.Creator, "Creator")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Description, propsDiff.Description, "Description")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Identifier, propsDiff.Identifier, "Identifier")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Keywords, propsDiff.Keywords, "Keywords")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Language, propsDiff.Language, "Language")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.LastModifiedBy, propsDiff.LastModifiedBy, "LastModifiedBy")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.LastPrinted, propsDiff.LastPrinted, "LastPrinted")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Modified, propsDiff.Modified, "Modified")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Revision, propsDiff.Revision, "Revision")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Subject, propsDiff.Subject, "Subject")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Title, propsDiff.Title, "Title")) != null)
                retPartDiff.Add(diff);

            if ((diff = CompareProperty(propsOriginal.Version, propsDiff.Version, "Version")) != null)
                retPartDiff.Add(diff);

            return retPartDiff;
        }

        private Uri GetCorePropertiesPart(Package package)
        {
            PackageRelationshipCollection relCool = package.GetRelationshipsByType("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties");
            PackageRelationship rel = null;
            short countCorePropRel = 0;
            foreach (PackageRelationship r in relCool)
            {
                rel = r;
                ++countCorePropRel;
            }

            if (countCorePropRel != 1)
                throw new ArgumentException("The specified package could not contain less or more than 1 core properties part !");

            return PackUriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri);
        }

        private DiffElement CompareProperty(string propOrig, string propDiff, string propertyName)
        {
            DiffElement retDiff = null;

            if (Utils.HasValue(propOrig) != Utils.HasValue(propDiff))
            {
                if (Utils.HasValue(propOrig))
                {
                    retDiff = new DiffElement(DiffType.Added, DiffElementType.Property);
                }
                else
                {
                    retDiff = new DiffElement(DiffType.Removed, DiffElementType.Property);
                }
                return retDiff;
            }
            else
            {
                if (Utils.HasValue(propOrig) && !propOrig.Equals(propDiff))
                {
                    retDiff = new DiffElement(DiffType.Modified, DiffElementType.Property)
                    {
                        Description = String.Format("{0}: '{1}' -> '{2}'",
                        propertyName,
                        propOrig,
                        propDiff)
                    };
                }
                else
                    return null;
            }
            return retDiff;
        }

        private DiffElement CompareProperty(DateTime? propOrig, DateTime? propDiff, string propertyName)
        {
            DiffElement retDiff = null;

            if (propOrig.HasValue != propDiff.HasValue)
            {
                if (propOrig.HasValue)
                {
                    retDiff = new DiffElement(DiffType.Added, DiffElementType.Property);
                }
                else
                {
                    retDiff = new DiffElement(DiffType.Removed, DiffElementType.Property);
                }
                return retDiff;
            }
            else
            {
                if (Utils.HasValue(propOrig) && (propOrig.Value != propDiff.Value))
                {
                    retDiff = new DiffElement(DiffType.Modified, DiffElementType.Property)
                    {
                        Description = String.Format("{0}: '{1}' -> '{2}'",
                        propertyName,
                        propOrig,
                        propDiff)
                    };
                }
                else
                    return null;
            }
            return retDiff;
        }

        #endregion

        #region Check parts

        private PartDiffCollection CheckPartsDiff(Package pkgOriginal, Package pkgDiff, AnalyzeLevel level)
        {
            PartDiffCollection retPartDiffs = new PartDiffCollection();

            // Check for addition in the diff package
            foreach (PackagePart partDiff in pkgDiff.GetParts())
            {
                if (!pkgOriginal.PartExists(partDiff.Uri) && 
                    (partDiff.ContentType != "application/vnd.openxmlformats-package.core-properties+xml"))
                {
                    PartDiff diffElem = new PartDiff(partDiff.Uri, partDiff.ContentType,
                        DiffType.Added);
                    retPartDiffs.Add(diffElem);
                }
            }

            // Check all the parts in the original package (deletion and modification)
            foreach (PackagePart partOriginal in pkgOriginal.GetParts())
            {
                // Ignore the core properties part
                if (partOriginal.ContentType == "application/vnd.openxmlformats-package.core-properties+xml")
                    continue;

                // Raise 'before analyzing' event
                OnPartAnalyzing(new PartEventArgs() { AnalyzePart = partOriginal });

                // Looking for deletion
                if (!pkgDiff.PartExists(partOriginal.Uri))
                {
                    PartDiff diffElem = new PartDiff(partOriginal.Uri, partOriginal.ContentType,
                        DiffType.Removed);
                    retPartDiffs.Add(diffElem);

                    // Raise 'After analyzing' event
                    OnPartAnalyzed(new PartEventArgs() { AnalyzePart = partOriginal });

                    continue;
                }

                // Looking for modification
                PackagePart partDiff = pkgDiff.GetPart(partOriginal.Uri);
                PartDiff partDifferences = CheckPartDiff(partOriginal, partDiff, level);

                // Check content type changes
                if (partOriginal.ContentType != partDiff.ContentType)
                    partDifferences.Add(
                        new DiffElement(DiffType.Modified,
                            DiffElementType.ContentType,
                            String.Format("Content type change from '{0}' to '{1}'",
                                partOriginal.ContentType,
                                partDiff.ContentType)));

                // If we detected some differences so we add part diffs to the returned collection.
                if (partDifferences.Count > 0)
                    retPartDiffs.Add(partDifferences);

                // Raise 'After analyzing' event
                OnPartAnalyzed(new PartEventArgs() { AnalyzePart = partOriginal });
            }

            return retPartDiffs;
        }

        private PartDiff CheckPartDiff(PackagePart partOriginal, PackagePart partDiff, AnalyzeLevel level)
        {
            if (partOriginal == null)
                throw new ArgumentNullException("partOriginal");

            PartDiff partDifferences =
                new PartDiff(partOriginal.Uri, partOriginal.ContentType, DiffType.Modified);

            /* Check for modification */

            // Check for type modification
            if (partOriginal.ContentType != partDiff.ContentType)
            {
                DiffElement diffElement = new DiffElement(DiffType.Modified,
                    DiffElementType.Part);
                partDifferences.Add(diffElement);
            }

            // Check for content modification
            PartAnalyzer analyzer = PartAnalyzer.GetAnalyzer(partOriginal);
            PartDiff contentPartDiff = analyzer.Analyze(partOriginal, partDiff, level);
            if (contentPartDiff.Count > 0)
            {
                // If the part content contains diffs then we add it to the return DiffPart object.
                foreach (DiffElement diffElem in contentPartDiff)
                    partDifferences.Add(diffElem);
            }

            if (partDifferences.Count > 0)
                return partDifferences;
            else
                return new PartDiff(partOriginal.Uri, partOriginal.ContentType, DiffType.Unchanged);
        }

        #endregion

        #region Event helpers

        private void OnPartAnalyzed(PartEventArgs eventArg)
        {
            EventHandler<PartEventArgs> handler = this.PartAnalyzed;
            if (handler != null)
                handler(this, eventArg);
        }

        private void OnPartAnalyzing(PartEventArgs eventArg)
        {
            EventHandler<PartEventArgs> handler = this.PartAnalyzing;
            if (handler != null)
                handler(this, eventArg);
        }

        #endregion
    }
}
